// 
// SoapTrace.Net - A soap message monitoring utility. 
// 
// Copyright (C) 2004-2009 Matthew Ward
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// Matthew Ward (mrward@users.sourceforge.net)
// 

using SoapTrace.Core.Diagnostics;
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;

namespace SoapTrace.Core.Communication
{
	/// <summary>
	/// A Tcp socket connection.
	/// </summary>
	public class TcpSocketConnection : IDisposable
	{
		/// <summary>
		/// Disposed flag.
		/// </summary>
		private bool m_Disposed;
		
		/// <summary>
		/// The underlying socket.
		/// </summary>
		private ISocket m_Socket;

		/// <summary>
		/// Bytes to send to the connected socket.
		/// </summary>
		private ByteArray m_BytesToSend = new ByteArray( );

		/// <summary>
		/// The buffer to read bytes into from one connection and
		/// then used to pass the bytes to the destination connection.
		/// </summary>
		private byte[ ] m_ReadBuffer = new byte[ 4096 ];
		
		/// <summary>
		/// Creates a new instance of the <see cref="TcpSocketConnection"/>
		/// class.
		/// </summary>
		/// <param name="address">The server's IP address.</param>
		/// <param name="port">The server's port.</param>
		public TcpSocketConnection( string address, int port )
		{
			Connect( address, port );
		}
		
		/// <summary>
		/// Creates a new instance of the <see cref="TcpSocketConnection"/>
		/// class.
		/// </summary>
		public TcpSocketConnection( )
		{
		}

		/// <summary>
		/// Creates a new instance of this class.
		/// </summary>
		/// <param name="socket">The connected socket.</param>
		public TcpSocketConnection( Socket socket )
		{
			m_Socket = CreateSocket( socket );
		}
		
		/// <summary>
		/// Gets the remote end point.
		/// </summary>
		public EndPoint RemoteEndPoint
		{
			get
			{
				return m_Socket.RemoteEndPoint;
			}
		}
		
		/// <summary>
		/// Gets the local end point.
		/// </summary>
		public EndPoint LocalEndPoint
		{
			get
			{
				return m_Socket.LocalEndPoint;
			}
		}		
		
		/// <summary>
		/// Connects to the specified IP address.
		/// </summary>
		/// <param name="address">An IP address.</param>
		/// <param name="port">The port.</param>
		public void Connect( string address, int port )
		{
			m_Socket = CreateSocket( AddressFamily.InterNetwork, 
				SocketType.Stream,
				ProtocolType.IP );
			
			IPHostEntry HostEntry = ResolveAddress( address );
			IPAddress IPAddress = HostEntry.AddressList[ 0 ];
			IPEndPoint EndPoint = new IPEndPoint( IPAddress, port );
			
			m_Socket.Connect( EndPoint );
		}

		/// <summary>
		/// Disposes this class.
		/// </summary>
		public void Dispose( )
		{
			Dispose( true );

			GC.SuppressFinalize( this );
		}
		
		/// <summary>
		/// Closes this connection.
		/// </summary>
		public void Close( )
		{
			if( m_Socket != null )
			{
				m_Socket.Shutdown( SocketShutdown.Both );
				m_Socket.Close( );
				m_Socket = null;
			}
		}
	
		/// <summary>
		/// Gets whether the connection is readable.
		/// </summary>
		public bool IsReadable
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Socket.Poll( 0, SelectMode.SelectRead );
			}
		}

		/// <summary>
		/// Gets whether the connection is writable.
		/// </summary>
		public bool IsWritable
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Socket.Poll( 0, SelectMode.SelectWrite );
			}
		}
		
		/// <summary>
		/// Gets whether the socket is connected.
		/// </summary>
		public bool IsConnected
		{
			get
			{
				bool IsConnected = false;
				
				if( m_Socket != null )
				{
					IsConnected = true;
				}
				
				return IsConnected;
			}
		}

		/// <summary>
		/// Gets whether there is any data to send.
		/// </summary>
		public bool HasDataToSend
		{
			[ DebuggerStepThrough ]
			get
			{
				bool HasData = false;

				if( m_BytesToSend.Length > 0 )
				{
					HasData = true;
				}

				return HasData;
			}
		}

		/// <summary>
		/// Sends data bytes.
		/// </summary>
		/// <remarks>This method attempts to send all the bytes 
		/// in one go, if it cannot then the bytes are kept for 
		/// sending at a later time.</remarks>
		public void Send( )
		{
			// Do we have any data to send?

			if( HasDataToSend )
			{
				// Can we send at the moment?

				if( IsWritable )
				{
					SendSocketData( );
				}
			}			
		}

		/// <summary>
		/// Adds bytes to send on calling <see cref="TcpSocketConnection.Send"/>.
		/// </summary>
		/// <param name="bytes">The bytes to add.</param>
		/// <param name="length">The number of bytes to add.</param>
		public void AddBytes( byte[ ] bytes, int length )
		{
			m_BytesToSend.Add( bytes, length );
		}

		/// <summary>
		/// Reads data sent to the socket.
		/// </summary>
		/// <returns>A flag indicating whether this connection is
		/// still open.</returns>
		public bool Read( )
		{
			return Read( null );
		}
		
		/// <summary>
		/// Reads data in from this connection and forwards it onto 
		/// the destination connection.
		/// </summary>
		/// <param name="destinationConnection">The connection where the
		/// read data should be delivered to.</param>
		/// <returns>A flag indicating whether this connection is
		/// still open.</returns>
		public bool Read( TcpSocketConnection destinationConnection )
		{
			bool IsOpen = true;

			if( IsReadable )
			{
				Logger.LogDebug( String.Concat( "Reading socket data.  RemoteEndPoint=", m_Socket.RemoteEndPoint.ToString( ) ) );
				
				// Read the source's response.

				int BytesRead = m_Socket.Receive( m_ReadBuffer );

				if( BytesRead > 0 )
				{
					// Inform derived class of bytes read.
					
					BytesReceived( m_ReadBuffer, BytesRead );

					// Add the read bytes to the destination.

					if( destinationConnection != null )
					{
						destinationConnection.AddBytes( m_ReadBuffer, BytesRead );
					}
				}
				else
				{
					IsOpen = false;
				} 
			}

			return IsOpen;
		}
		
		/// <summary>
		/// Resolves the IP address using DNS.
		/// </summary>
		/// <param name="address">An IP address or host name.</param>
		/// <returns>An <see cref="IPHostEntry"/>.</returns>
		protected virtual IPHostEntry ResolveAddress( string address )
		{
			return Dns.GetHostEntry( address );
		}
		
		/// <summary>
		/// Creates a new socket connection.
		/// </summary>
		/// <param name="addressFamily">One of the <see cref="AddressFamily"/>
		///  values.</param>
		/// <param name="socketType">One of the <see cref="SocketType"/>
		///  values.</param>
		/// <param name="protocolType">One of the <see cref="ProtocolType"/>
		///  values.</param>
		/// <returns>An object that implements the <see cref="ISocket"/>
		/// interface.</returns>
		protected virtual ISocket CreateSocket( AddressFamily addressFamily,
			SocketType socketType,
			ProtocolType protocolType )
		{
			return new TcpSocket( addressFamily, socketType, protocolType );
		}		
		
		/// <summary>
		/// Creates socket from an already connected socket.
		/// </summary>
		protected virtual ISocket CreateSocket( Socket socket )
		{
			return new TcpSocket( socket );
		}

		/// <summary>
		/// Sends any data needing to be sent to the connection.
		/// </summary>
		private void SendSocketData( )
		{
			byte[ ] Bytes = m_BytesToSend.GetBytes( );
			
			Logger.LogDebug( String.Concat( "Sending socket data.", m_Socket.RemoteEndPoint.ToString( ) ) );
			
			int BytesWritten = m_Socket.Send( Bytes );

			// Inform derived class of bytes sent.

			BytesSent( Bytes, BytesWritten );
			
			m_BytesToSend.ShiftLeft( BytesWritten );
		}

		/// <summary>
		/// A callback method with the bytes that have been sent from
		/// the client to the proxy server.  Derived classes can
		/// override this method to do their own processing of the messages.
		/// </summary>
		/// <param name="bytes">An array of bytes.</param>
		/// <param name="length">The number of bytes the array contains.</param>
		protected virtual void BytesReceived( byte[ ] bytes, int length )
		{
		}

		/// <summary>
		/// A callback method with the bytes that have been sent to
		/// the client from the proxy server.  Derived classes can
		/// override this method to do their own processing of the messages.
		/// </summary>
		/// <param name="bytes">An array of bytes.</param>
		/// <param name="length">The number of bytes the array contains.</param>
		protected virtual void BytesSent( byte[ ] bytes, int length )
		{
		}
		
		/// <summary>
		/// Disposes managed and unmanaged objects. 
		/// </summary>
		/// <param name="disposing">If <see langword="true"/> the method has been called directly
		/// or indirectly by a user's code. Managed and unmanaged resources
		/// can be disposed.
		/// If disposing equals <see langword="false"/>, the method has been called by the 
		/// runtime from inside the finalizer and you should not reference 
		/// other objects. Only unmanaged resources can be disposed.
		/// </param>
		private void Dispose( bool disposing )
		{
			if( !m_Disposed )
			{
				if( disposing )
				{
					// Dispose managed resources.
					
					Close( );
				}
			}

			m_Disposed = true;         
		}		
	}
}
